﻿using System;

namespace Aeon.Emulator.Sound.FM.Operators
{
    /// <summary>
    /// Generates an ADSR envelope for waveform data.
    /// </summary>
    internal abstract class OperatorBase
    {
        protected static readonly double[] PhaseMultipliers = { 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15 };

        private readonly int registerOffset;
        private readonly FmSynthesizer opl;
        private double linearAttackIncrement;
        private double linearMinAttack;
        private double decibelsDecayIncrement;
        private double decibelsReleaseIncrement;
        private double attenuation;
        private double calculatedSustainLevel;
        private double linearAttackValue;
        private double envelope;
        private double phaseIncrement;
        private int keyScaleNumber;
        private int frequencyNumber;
        private int block;

        /// <summary>
        /// Initializes a new instance of the <see cref="OperatorBase"/> class.
        /// </summary>
        /// <param name="baseAddress">The base address of the operator's control registers.</param>
        /// <param name="opl">The synthesizer which owns the operator.</param>
        protected OperatorBase(int baseAddress, FmSynthesizer opl)
        {
            this.registerOffset = baseAddress;
            linearAttackValue = Math.Log(96.0, 2.0);
            envelope = -96;
            this.opl = opl;
        }

        /// <summary>
        /// Gets or sets the current ADSR state.
        /// </summary>
        public AdsrState AdsrState { get; private set; }
        /// <summary>
        /// Gets or sets the current phase of the operator.
        /// </summary>
        public double Phase { get; protected set; }

        /// <summary>
        /// Gets or sets the current ADSR envelope value.
        /// </summary>
        protected double Envelope { get; set; }
        /// <summary>
        /// Gets the currently selected waveform.
        /// </summary>
        protected OperatorWaveform SelectedWaveform
        {
            get { return OperatorWaveform.GetWaveform(this.WaveformSelection & ((this.opl.Opl3Mode << 2) + 3)); }
        }

        #region Registers
        public int Multiplier
        {
            get { return this.opl.Registers[registerOffset + 0x20] & 0x0F; }
        }
        public int AttackRelease
        {
            get { return (this.opl.Registers[registerOffset + 0x60] & 0xF0) >> 4; }
            set
            {
                int registerValue = this.opl.Registers[registerOffset + 0x60] & 0x0F;
                this.opl.Registers[registerOffset + 0x60] = (byte)(registerValue | (value << 4));
            }
        }

        private int WaveformSelection
        {
            get { return this.opl.Registers[registerOffset + 0xE0] & 0x07; }
        }
        private int DecayRate
        {
            get { return this.opl.Registers[registerOffset + 0x60] & 0x0F; }
        }
        private int ReleaseRate
        {
            get { return this.opl.Registers[registerOffset + 0x80] & 0x0F; }
        }
        private int KeyScaleRatio
        {
            get { return (this.opl.Registers[registerOffset + 0x20] & 0x10) >> 4; }
        }
        private bool VibratoEnabled
        {
            get { return (this.opl.Registers[registerOffset + 0x20] & 0x40) != 0; }
        }
        private int KeyScaleLevel
        {
            get { return (this.opl.Registers[registerOffset + 0x40] & 0xC0) >> 6; }
        }
        private int TotalLevel
        {
            get { return this.opl.Registers[registerOffset + 0x40] & 0x3F; }
        }
        private int SustainLevel
        {
            get { return this.opl.Registers[registerOffset + 0x80] >> 4; }
        }
        private bool TremoloEnabled
        {
            get { return (this.opl.Registers[registerOffset + 0x20] & 0x80) != 0; }
        }
        private bool HoldSustainEnabled
        {
            get { return (this.opl.Registers[registerOffset + 0x20] & 0x20) != 0; }
        }
        #endregion

        /// <summary>
        /// Set Channel-related register values.
        /// </summary>
        /// <param name="frequencyNumber">The current frequency number.</param>
        /// <param name="keyScaleNumber">The current key scale number.</param>
        /// <param name="block">The current block number.</param>
        public void SetChannelData(int frequencyNumber, int keyScaleNumber, int block)
        {
            this.keyScaleNumber = keyScaleNumber;
            this.frequencyNumber = frequencyNumber;
            this.block = block;
            this.Recalculate();
        }
        /// <summary>
        /// Refreshes all of the calculated values in the operator.
        /// </summary>
        public void Recalculate()
        {
            int ksr = this.KeyScaleRatio;

            #region Phase Increment
            {
                var baseFrequency = ((this.frequencyNumber * (1 << (block - 1))) * opl.SampleRate) / (1 << 19);
                var operatorFrequency = baseFrequency * PhaseMultipliers[this.Multiplier];

                this.phaseIncrement = operatorFrequency / opl.SampleRate;
            }
            #endregion

            #region Attack Rate
            {
                int actualAttackRate = CalculateRate(this.AttackRelease, ksr, this.keyScaleNumber);
                var fullPeriod = AdsrConstants.AttackTimes[actualAttackRate, 0] / 1000.0;

                int fullPeriodSamples = (int)(fullPeriod * opl.SampleRate);
                var expPeriod = AdsrConstants.AttackTimes[actualAttackRate, 1] / 1000.0;

                int expPeriodSamples = (int)(expPeriod * opl.SampleRate);
                this.linearAttackIncrement = AdsrConstants.ExpRangeLinear / this.opl.SampleRate * (1.0 / expPeriod);

                int endSamples = (int)(expPeriodSamples + (AdsrConstants.SampleAttackFactor - AdsrConstants.NinetyPercentLinear) / this.linearAttackIncrement);
                this.linearMinAttack = AdsrConstants.TenPercentLinear - (fullPeriodSamples - endSamples) * this.linearAttackIncrement;
            }
            #endregion

            #region Decay and Release
            {
                this.decibelsDecayIncrement = this.RecalculateDecayReleaseIncrement(this.DecayRate, ksr, this.keyScaleNumber);
                this.decibelsReleaseIncrement = this.RecalculateDecayReleaseIncrement(this.ReleaseRate, ksr, this.keyScaleNumber);
            }
            #endregion

            #region Attenuation
            {
                int ksl = this.KeyScaleLevel;

                if(ksl == 0)
                    this.attenuation = 0;
                else
                {
                    var factor = AdsrConstants.AttenuationScaleFactors[(this.frequencyNumber >> 6) & 0x0F, block];
                    if(ksl == 1)
                        this.attenuation = factor;
                    else if(ksl == 2)
                        this.attenuation = factor / 2;
                    else
                        this.attenuation = factor * 2;
                }
            }
            #endregion

            #region Sustain Level
            {
                int sustainLevel = this.SustainLevel;
                this.calculatedSustainLevel = sustainLevel == 0x0F ? -93 : -3 * sustainLevel;
            }
            #endregion
        }
        /// <summary>
        /// Activates the operator.
        /// </summary>
        public void KeyOn()
        {
            if(this.AttackRelease > 0)
            {
                double xCurrent = Math.Log(-envelope, 2.0);
                linearAttackValue = Math.Min(xCurrent, linearMinAttack);
                this.AdsrState = AdsrState.Attack;
                this.Phase = 0;
            }
            else
                this.AdsrState = AdsrState.Off;
        }
        /// <summary>
        /// Disables the operator.
        /// </summary>
        public void KeyOff()
        {
            if(this.AdsrState != AdsrState.Off)
                this.AdsrState = AdsrState.Release;
        }

        /// <summary>
        /// Recalculates the phase of the operator.
        /// </summary>
        protected void RecalculatePhase()
        {
            if(this.VibratoEnabled)
                this.Phase += phaseIncrement * this.opl.VibratoMultiplier;
            else
                this.Phase += phaseIncrement;

            this.Phase %= 1;
        }
        /// <summary>
        /// Applies the ADSR envelope to the current operator value.
        /// </summary>
        /// <returns>Operator value after applying the ADSR envelope.</returns>
        protected double GetEnvelope()
        {
            switch(this.AdsrState)
            {
            case AdsrState.Attack:
                if(this.envelope < -0.1875 && this.linearAttackIncrement != -double.PositiveInfinity)
                {
                    this.envelope = -Math.Pow(2, linearAttackValue);
                    this.linearAttackValue += linearAttackIncrement;
                    break;
                }
                else
                {
                    this.envelope = 0;
                    this.AdsrState = AdsrState.Decay;
                    goto case AdsrState.Decay;
                }

            case AdsrState.Decay:
                if(this.envelope > this.calculatedSustainLevel / 2)
                {
                    this.envelope -= this.decibelsDecayIncrement;
                    break;
                }
                else
                {
                    this.AdsrState = AdsrState.Sustain;
                    goto case AdsrState.Sustain;
                }

            case AdsrState.Sustain:
                if(!this.HoldSustainEnabled)
                {
                    if(this.envelope > -96)
                        this.envelope -= this.decibelsReleaseIncrement;
                    else
                        this.AdsrState = AdsrState.Off;
                }
                break;

            case AdsrState.Release:
                if(this.envelope > -96)
                    this.envelope -= this.decibelsReleaseIncrement;
                else
                    this.AdsrState = AdsrState.Off;
                break;
            }

            var outputEnvelope = this.envelope;

            if(this.TremoloEnabled)
                outputEnvelope += this.opl.TremoloMultiplier / 2; //this.opl.tremoloTable[this.opl.dam, this.opl.tremoloIndex] / 2;

            outputEnvelope += (this.attenuation + (this.TotalLevel * -0.75)) / 2;

            return ((int)(outputEnvelope / 0.1875)) * 0.1875;
        }

        private static int CalculateRate(int rate, int ksr, int keyScaleNumber)
        {
            int rateOffset = ksr == 0 ? keyScaleNumber / 4 : keyScaleNumber;
            int actualRate = rate * 4 + rateOffset;
            return actualRate & 63;
        }

        private double RecalculateDecayReleaseIncrement(int rate, int ksr, int keyScaleNumber)
        {
            int calculatedRate = CalculateRate(rate, ksr, keyScaleNumber);
            var period = AdsrConstants.DecayReleaseTimes[calculatedRate, 1] / 1000.0;
            return AdsrConstants.ExpRangeDecibels / this.opl.SampleRate * (1.0 / period);
        }
    }
}
